// Written in the D Programming Language
/**
 * The 9th lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=09">NeHe tutorial series</a>.
 * Originally written by Jeff Molofee.
 *
 * Authors: Jeff Molofee
 *          Olli Aalto
 */
module lesson09;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.sdl.sdl;

import tango.stdc.stringz;
import tango.math.random.Kiss;

/// The window title
const char[]
	WINDOW_TITLE = "NeHe's Animated Blended Textures Tutorial (D version)";

/// The main loop flag
bool running = true;

/// Twinkling Stars ON / OFF
bool twinkle;

/// The random number generator.
Kiss kiss;

bool pageUp;
bool pageDown;
bool up;
bool down;

/// Create A Struct For Star
struct Star
{
	/// Stars Colors
	int r, g, b;
	/// Stars Distance From Center
	GLfloat dist;
	/// Stars Current Angle
	GLfloat angle;
}

/// Make 'stars' Array Of 10 Using Info From The Struct 'Star' 
Star stars[10];

/// Viewing Distance Away From Stars
GLfloat zoom = -15.0f;
/// Tilt The View
GLfloat tilt = 90.0f;
/// Spin Twinkling Stars
GLfloat spin = 0.0f;

/// Storage For One Texture
GLuint texture;

/**
 * Module constructor. Here we load the GL, GLU and SDL shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();

	if (SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}
	// Initialize the random number generator
	kiss = Kiss();

	// Create A Loop That Goes Through All The Stars
	// Here we need to remember to use 'inout', otherwise the settings
	// don't stay.
	foreach (i, inout star; stars)
	{
		// Start All The Stars At Angle Zero
		star.angle = 0.0f;
		// Calculate Distance From The Center
		star.dist = cast(float) i / stars.length * 5.0f;
		// Give star A Random Red Intensity
		star.r = kiss.toInt() % 256;
		// Give star A Random Green Intensity
		star.g = kiss.toInt() % 256;
		// Give star A Random Blue Intensity
		star.b = kiss.toInt() % 256;
	}
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;

	if (args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	createGLWindow(WINDOW_TITLE, 640, 480, 16, fullScreen);
	initGL();

	while (running)
	{
		processEvents();

		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);
	}
}

/**
 * function to load in bitmap as a GL texture.
 */
void loadGLTextures()
{
	/* Create storage space for the texture */
	SDL_Surface* textureImage;

	/* Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit */
	if ((textureImage = SDL_LoadBMP("data/Star.bmp")) !is null)
	{
		// Free the surface when exiting the scope
		scope(exit)
			SDL_FreeSurface(textureImage);

		/* Create The Texture */
		glGenTextures(1, &texture);

		/* Typical Texture Generation Using Data From The Bitmap */
		glBindTexture(GL_TEXTURE_2D, texture);

		/* Linear Filtering */
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		/* Generate The Texture */
		glTexImage2D(GL_TEXTURE_2D, 0, 3, textureImage.w, textureImage.h, 0,
			GL_BGR, GL_UNSIGNED_BYTE, textureImage.pixels);

		return;
	}
	throw new Exception("Failed to load texture.");
}

/**
 * Process all the pending events.
 */
void processEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_KEYUP:
				keyReleased(event.key.keysym.sym);
				break;
			case SDL_KEYDOWN:
				keyPressed(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			default:
				break;
		}
	}

	if (up)
	// Is Up Arrow Being Pressed
	{
		tilt -= 0.5f; // Tilt The Screen Up
	}

	if (down)
	// Is Down Arrow Being Pressed
	{
		tilt += 0.5f; // Tilt The Screen Down
	}

	if (pageUp)
	// Is Page Up Being Pressed
	{
		zoom -= 0.2f; // Zoom Out
	}

	if (pageDown)
	// Is Page Down Being Pressed
	{
		zoom += 0.2f; // Zoom In
	}
}

void keyPressed(int key)
{
	switch (key)
	{
		case SDLK_PAGEUP:
			pageUp = true;
			break;
		case SDLK_PAGEDOWN:
			pageDown = true;
			break;
		case SDLK_UP:
			up = true;
			break;
		case SDLK_DOWN:
			down = true;
			break;
		default:
			break;
	}
}

void keyReleased(int key)
{
	switch (key)
	{
		case SDLK_ESCAPE:
			running = false;
			break;
		case SDLK_t:
			twinkle = !twinkle;
			break;
		case SDLK_PAGEUP:
			pageUp = false;
			break;
		case SDLK_PAGEDOWN:
			pageDown = false;
			break;
		case SDLK_UP:
			up = false;
			break;
		case SDLK_DOWN:
			down = false;
			break;
		default:
			break;
	}
}

/**
 * Resize And Initialize The GL Window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	if (height == 0)
	{
		height = 1;
	}
	// Reset The Current Viewport
	glViewport(0, 0, width, height);

	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);
	// Reset The Projection Matrix
	glLoadIdentity();

	// Calculate The Aspect Ratio Of The Window
	gluPerspective(45.0f, cast(GLfloat) width / cast(GLfloat) height, 0.1f,
		100.0f);

	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);
	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * All Setup For OpenGL Goes Here.
 */
void initGL()
{
	loadGLTextures();

	// Enable Texture Mapping
	glEnable(GL_TEXTURE_2D);
	// Enable Smooth Shading
	glShadeModel(GL_SMOOTH);
	// Black Background
	glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
	// Depth Buffer Setup
	glClearDepth(1.0f);
	// Really Nice Perspective Calculations
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
	// Set The Blending Function For Translucency
	glBlendFunc(GL_SRC_ALPHA, GL_ONE);
	// Enable Blending
	glEnable(GL_BLEND);

	// Change to texture matrix and flip and rotate the texture
	glMatrixMode(GL_TEXTURE);
	glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
	glScalef(-1.0f, 1.0f, 1.0f);
	// Back to normal
	glMatrixMode(GL_MODELVIEW);
}

/**
 * Here's Where We Do All The Drawing.
 */
void drawGLScene()
{
	// Clear The Screen And The Depth Buffer
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	// Select Our Texture
	glBindTexture(GL_TEXTURE_2D, texture);

	// Loop Through All The Stars
	// Here too we need to use the 'inout' because we change the values
	// of the star
	foreach (i, inout star; stars)
	{
		// Reset The View Before We Draw Each Star
		glLoadIdentity();

		// Zoom Into The Screen (Using The Value In 'zoom')
		glTranslatef(0.0f, 0.0f, zoom);

		// Tilt The View (Using The Value In 'tilt')
		glRotatef(tilt, 1.0f, 0.0f, 0.0f);
		// Rotate To The Current Stars Angle
		glRotatef(star.angle, 0.0f, 1.0f, 0.0f);

		// Move Forward On The X Plane
		glTranslatef(star.dist, 0.0f, 0.0f);

		// Cancel The Current Stars Angle
		glRotatef(-star.angle, 0.0f, 1.0f, 0.0f);
		// Cancel The Screen Tilt
		glRotatef(-tilt, 1.0f, 0.0f, 0.0f);

		if (twinkle)
		// Twinkling Stars Enabled
		{
			int other = stars.length - i - 1;
			// Assign A Color Using Bytes
			glColor4ub(stars[other].r, stars[other].g, stars[other].b, 255);

			glBegin(GL_QUADS); // Begin Drawing The Textured Quad
			glTexCoord2f(0.0f, 0.0f);
			glVertex3f(-1.0f, -1.0f, 0.0f);
			glTexCoord2f(1.0f, 0.0f);
			glVertex3f(1.0f, -1.0f, 0.0f);
			glTexCoord2f(1.0f, 1.0f);
			glVertex3f(1.0f, 1.0f, 0.0f);
			glTexCoord2f(0.0f, 1.0f);
			glVertex3f(-1.0f, 1.0f, 0.0f);
			glEnd(); // Done Drawing The Textured Quad
		}
		// Rotate The Star On The Z Axis
		glRotatef(spin, 0.0f, 0.0f, 1.0f);

		// Assign A Color Using Bytes
		glColor4ub(star.r, star.g, star.b, 255);
		glBegin(GL_QUADS); // Begin Drawing The Textured Quad
		glTexCoord2f(0.0f, 0.0f);
		glVertex3f(-1.0f, -1.0f, 0.0f);
		glTexCoord2f(1.0f, 0.0f);
		glVertex3f(1.0f, -1.0f, 0.0f);
		glTexCoord2f(1.0f, 1.0f);
		glVertex3f(1.0f, 1.0f, 0.0f);
		glTexCoord2f(0.0f, 1.0f);
		glVertex3f(-1.0f, 1.0f, 0.0f);
		glEnd(); // Done Drawing The Textured Quad

		// Used To Spin The Stars
		spin += 0.01f;
		// Changes The Angle Of A Star
		star.angle += cast(float) i / stars.length;
		// Changes The Distance Of A Star
		star.dist -= 0.01f;

		// Is The Star In The Middle Yet
		if (star.dist < 0.0f)
		{
			// Move The Star 5 Units From The Center
			star.dist += 5.0f;
			// Give It A New Red Value
			star.r = kiss.toInt() % 256;
			// Give It A New Green Value
			star.g = kiss.toInt() % 256;
			// Give It A New Blue Value
			star.b = kiss.toInt() % 256;
		}
	}
}

void createGLWindow(char[] title, int width, int height, int bits,
	bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	/* Note the SDL_DOUBLEBUF flag is not required to enable double
	 * buffering when setting an OpenGL video mode.
	 * Double buffering is enabled or disabled using the
	 * SDL_GL_DOUBLEBUFFER attribute.
	 */
	int mode = SDL_OPENGL;
	if (fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if (SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}
